const BN = require('bn.js');

const web3 = require('./web3');
const db_manager = require('./db_manager');
const utils = require('./utils');
const bn128 = require('./bn128');
const conman = require('./contract_manager');
const burn = require('./burn');
const transfer = require('./transfer');

module.exports = exports = {};

exports.before_parse = function before(program, db) {
  const comm = program.command('zth');

  comm.command('list')
    .description('list known zth accounts')
    .action(() => {
      const db = db_manager.load_db(program.opts().datadir);
      const zthaccs = db.get('zthaccs').cloneDeep().value();
      const entries = Object.entries(zthaccs);
      if (entries.length === 0) {
        console.log('no zth account yet');
      }
      for (const [yhash, zthacc] of entries) {
        console.log(yhash, ':', zthacc);
      }
    });

  comm.command('create')
    .description('create new zth account')
    .action(() => {
      const db = db_manager.load_db(program.opts().datadir);
      const keypair = utils.createAccount();
      const x = bn128.bytes(keypair.x.fromRed());
      const yhash = utils.hashPoint(keypair.y);
      const zthacc = {
        ...keypair,
        x,
        yhash,
      };
      db.set(`zthaccs.${yhash}`, zthacc).write();
      console.log(`new zth account created:`, zthacc);
    });


  comm.command('register <yhash> <contract_address>')
    .alias('r')
    .description('register zth account to a zether contract')
    .option('--sender <sender>',
            'eth message sender, or env.ZETHER_ETHACC',
            process.env.ZETHER_ETHACC)
    .option('--no-send', 'using call instead of send')
    .action(async (yhash, contract_address, opts) => {
      const db = db_manager.load_db(program.opts().datadir);
      const zthacc = db.get(`zthaccs.${yhash}`).value();
      if (!zthacc) {
        throw Error('yhash N/A');
      }

      if (!opts.sender) {
        throw Error('no sender');
      }

      try {
        const keypair = utils.unserialize_keypair(zthacc);
        if (opts.send) {
          // await web3.eth.personal.unlockAccount(opts.sender, opts.pass, 30);
          const res = await conman.register(keypair, contract_address, opts.sender);
          console.log('receipt:', res);
        } else {
          const res = await conman.register_sim(keypair, contract_address);
          console.log('result:', res);
        }
      } catch (err) {
        console.log('error:', err.message);
      }
    });

  comm.command('balance <yhash> <contract_address>')
    .description('read balance of zth account')
    .option('--min <min>')
    .action(async (yhash, contract_address, options) => {
      const db = db_manager.load_db(program.opts().datadir);
      const zthacc = db.get(`zthaccs.${yhash}`).value();
      if (!zthacc) {
        throw Error('yhash N/A');
      }

      options.min = Number.parseInt(options.min);
      if (Number.isNaN(options.min)) {
        options.min = 0;
      }

      try {
        const keypair = utils.unserialize_keypair(zthacc);
        const res = await conman.balance(keypair, contract_address);
        const bal = utils.readBalance(res[0], res[1], keypair.x, options.min);
        console.log(`balance: ${bal}`);
      } catch (err) {
        throw err;
      }
    });

  comm.command('bare <yhash> <contract_address>')
    .description('display acc and pending')
    .action(async (yhash, contract_address, options) => {
      const db = db_manager.load_db(program.opts().datadir);
      const zthacc = db.get(`zthaccs.${yhash}`).value();
      if (!zthacc) {
        throw Error('yhash N/A');
      }

      try {
        const keypair = utils.unserialize_keypair(zthacc);
        const res = await conman.bare(keypair, contract_address);
        const acc = {
          C: {
            x: res[0][0].x,
            y: res[0][0].y,
          },
          D: {
            x: res[0][1].x,
            y: res[0][1].y,
          }
        };

        const pending = {
          C: {
            x: res[1][0].x,
            y: res[1][0].y,
          },
          D: {
            x: res[1][1].x,
            y: res[1][1].y,
          }
        };
        console.log('acc: ', acc);
        console.log('pending: ', pending);

        /*
        const accC = bn128.unserialize(res[0][0]);
        const accD = bn128.unserialize(res[0][1]);
        const pendingC = bn128.unserialize(res[1][0]);
        const pendingD = bn128.unserialize(res[1][1]);

        console.log(accC.x.toString(16), accC.y.toString(16));
        */

        // console.log('acc C:', `(${accC.x.toString(16)}, ${accC.y.toString(16)})`);
        // console.log('acc D:', `(${accD.x.toString(16)}, ${accD.y.toString(16)})`);
        // console.log('pen C:', `(${pendingC.x.toString(16)}, ${pendingC.y.toString(16)})`);
        // console.log('pen D:', `(${pendingD.x.toString(16)}, ${pendingD.y.toString(16)})`);
      } catch (err) {
        throw err;
      }
    });

  comm.command('fund <yhash> <contract_address> <b_in>')
    .description('deposit values into zth account, contract owner only')
    .option('--sender <sender>',
            'eth message sender, or env.ZETHER_ETHACC',
            process.env.ZETHER_ETHACC)
    .option('--no-send', 'using call instead of send')
    .action(async (yhash, contract_address, b_in, opts) => {
      const db = db_manager.load_db(program.opts().datadir);
      const zthacc = db.get(`zthaccs.${yhash}`).value();
      if (!zthacc) {
        throw Error('yhash N/A');
      }

      if (!opts.sender) {
        console.log(opts);
        throw Error('no sender');
      }

      const b_in_int = Number.parseInt(b_in);
      if (Number.isNaN(b_in_int)) {
        throw Error('b_in N/A');
      }


      try {
        const keypair = utils.unserialize_keypair(zthacc);

        if (opts.send) {
          // await web3.eth.personal.unlockAccount(opts.sender, opts.pass, 30);
          const res = await conman.fund(keypair, b_in_int, contract_address, opts.sender);
          console.log('receipt:', res);
        } else {
          const b = await conman.fund_sim(keypair, b_in_int, contract_address, opts.sender);
          console.log('balance increased:', b);
        }
      } catch (err) {
        throw err;
      }
    });

  comm.command('burn <yhash> <contract_address> <b_out>')
    .option('--sender <sender>',
            'eth message sender, or env.ZETHER_ETHACC',
            process.env.ZETHER_ETHACC)
    .option('--no-send')
    .description('burn values from zth, contract owner only')
    .action(async (yhash, contract_address, b_out, opts) => {
      const db = db_manager.load_db(program.opts().datadir);
      const zthacc = db.get(`zthaccs.${yhash}`).value();
      if (!zthacc) {
        throw Error('yhash N/A');
      }

      if (!opts.sender) {
        console.log(opts);
        throw Error('no sender');
      }

      opts.min = Number.parseInt(opts.min);
      if (Number.isNaN(opts.min)) {
        opts.min = 0;
      }

      const b_out_int = Number.parseInt(b_out);
      if (Number.isNaN(b_out_int)) {
        throw Error('b_out N/A');
      }

      try {
        const keypair = utils.unserialize_keypair(zthacc);

        const info = await conman.info_zether(contract_address);
        const epoch_len = info[2];
        const current = (new Date).getTime(); // in millisecond
        let epoch = Math.floor(current / (1000 * epoch_len));
        const away = Math.ceil(current / (1000 * epoch_len)) * 1000 * epoch_len - current;
        console.log(`epoch(len: ${epoch_len}): ${epoch}, away: ${away}`);
        // console.log(`options: `, opts);
        if (away < epoch_len * 400) { // wait till next epoch
          console.log(`task queued ... after ${away} milliseconds`);
          await new Promise(resolve => setTimeout(resolve, away));
          epoch = epoch + 1;
        }
        const bal = await conman.balance(keypair, contract_address, epoch);
        const bal_int = utils.readBalance(bal[0], bal[1], keypair['x'], opts.min);
        const b_diff = bal_int - b_out_int;
        console.log(`will do: ${b_diff} == ${bal_int} - ${b_out_int}`);
        const CLn = bn128.serialize(bn128.unserialize(bal[0]).add(bn128.curve.g.mul(new BN(-b_out_int))));
        const CRn = bal[1];
        const u = bn128.serialize(utils.u(epoch, keypair['x']));

        const proof = burn.proveBurn(CLn, CRn, keypair['y'], epoch, opts.sender, keypair['x'], b_diff);
        // await web3.eth.personal.unlockAccount(opts.sender, opts.pass, 30);
        if (opts.send) {
          const res = await conman.burn(keypair, b_out_int, u, proof, contract_address, opts.sender);
          console.log(res);
          // const events = res.events;
          // res.events = null;
          // for (const [i, v] of Object.entries(events)) {
          //   console.log(i, v);
          // }
        } else {
          const res = await conman.burn_sim(keypair, b_out_int, u, proof, contract_address, opts.sender);
          console.log('simulated res: ', res);
          // TODO: change BurnVerifier and see the sigma
        }
      } catch (err) {
        // console.log('error: ', err);
        throw err;
      }
    });

  comm.command('transfer <from_yhash> <to_yhash> <contract_address> <b_val>')
    .alias('t')
    .option('--sender <sender>',
            'eth message sender, or env.ZETHER_ETHACC',
            process.env.ZETHER_ETHACC)
    .option('--decoys <decoys...>', 'decoy accounts')
    .option('--no-send')
    .description('burn values from zth, contract owner only')
    .action(async (from_yhash, to_yhash, contract_address, b_val, opts) => {
      if (from_yhash == to_yhash) {
        throw Error('from_yhash should not equal to_yhash');
      }

      const db = db_manager.load_db(program.opts().datadir);
      const from_zthacc = db.get(`zthaccs.${from_yhash}`).value();
      if (!from_zthacc) {
        throw Error('unknown from yhash');
      }
      const to_zthacc = db.get(`zthaccs.${to_yhash}`).value();
      if (!to_zthacc) {
        throw Error('unknown yhash to');
      }

      if (!opts.sender) {
        // console.log(opts);
        throw Error('no sender');
      }

      b_val = Number.parseInt(b_val);
      if (Number.isNaN(b_val)) {
        throw Error('invalid b_val');
      }
      console.log(`to transfer: ${b_val}`);

      opts.min = Number.parseInt(opts.min);
      if (Number.isNaN(opts.min)) {
        opts.min = 0;
      }

      if (!opts.decoys) {
        opts.decoys = [];
      }
      let size = 2 + opts.decoys.length;
      if (!([2,4,8,16].includes(size))) {
        throw Error('total size should be in [2,4,8,16]');
      }

      const zthaccs = [from_zthacc, to_zthacc];

      for (const i in opts.decoys) {
        const decoy_yhash = opts.decoys[i];
        console.log(`decoy[${i}]: ${decoy_yhash}`);
        const decoy_zthacc = db.get(`zthaccs.${decoy_yhash}`).value();
        if (!decoy_zthacc) {
          throw Error(`decoy_yhash N/A: ${decoy_yhash}`);
        }

        for (const j in zthaccs) {
          if (decoy_yhash.localeCompare(zthaccs[j].yhash) === 0) {
            throw Error(`duplicated yhash: ${decoy_yhash}`);
          }
        }
        zthaccs.push(decoy_zthacc);
      }

/*
      var pf = () => {
        for (const i in zthaccs) {
          console.log(zthaccs[i].yhash);
        }
      }

      console.log('before random:');
      pf();
*/

      let from_index = -1;
      let to_index = -1;

      let m = size;
      while (m != 0) {
        const i = Math.floor(Math.random() * m); // integer from [0, m-1]
        m = m - 1;
        const temp = zthaccs[i];
        zthaccs[i] = zthaccs[m];
        zthaccs[m] = temp;
        if (temp.yhash.localeCompare(from_yhash) === 0) {
          from_index = m;
        } else if (temp.yhash.localeCompare(to_yhash) === 0) {
          to_index = m;
        }
      }

      if (from_index % 2 === to_index % 2) {
        const offset = (to_index % 2 === 0) ? 1 : -1;
        const next_index = to_index + offset;
        const temp = zthaccs[to_index];
        zthaccs[to_index] = zthaccs[next_index];
        zthaccs[next_index] = temp;
        to_index = next_index;
      }

      // console.log('after random:', from_index, to_index);
      // pf();

      try {
        const info = await conman.info_zether(contract_address);
        const epoch_len = info[2];
        const current = (new Date).getTime(); // in millisecond
        let epoch = Math.floor(current / (1000 * epoch_len));
        const away = Math.ceil(current / (1000 * epoch_len)) * 1000 * epoch_len - current;
        console.log(`epoch(len: ${epoch_len}): ${epoch}, away: ${away}`);
        // console.log(`options: `, opts);
        if (away < epoch_len * 400) { // wait till next epoch
          console.log(`task queued ... after ${away} milliseconds`);
          await new Promise(resolve => setTimeout(resolve, away));
          epoch = epoch + 1;
        }
        const keypairs = zthaccs.map(utils.unserialize_keypair);
        const ys = keypairs.map((kp) => kp.y);
        const bals = await conman.balances(ys, contract_address, epoch);
        const from_bal = utils.readBalance(bals[from_index][0],
                                           bals[to_index][1],
                                           keypairs[from_index]['x'],
                                           opts.min);
        
        // construct data structures
        const r = bn128.randomScalar(); // random r mod q
        const C = ys.map((y, i) => {
          let value = 0;
          if (i === from_index) {
            value = -b_val;
          } else if (i === to_index) {
            value = b_val;
          }
          return bn128.curve.g.mul(new BN(value)).add(bn128.unserialize(y).mul(r));
        });
        const D = bn128.curve.g.mul(r);
        const CLn = [];
        const CRn = [];
        for (const [i, bal] of Object.entries(bals)) {
          CLn.push(bn128.serialize(bn128.unserialize(bal[0]).add(C[i])));
          CRn.push(bn128.serialize(bn128.unserialize(bal[1]).add(D)));
        }

        C_serialized = C.map(bn128.serialize);
        D_serialized = bn128.serialize(D);
        const proof = transfer.proveTransfer(CLn, CRn,
                                             C_serialized, D_serialized,
                                             ys, epoch, keypairs[from_index].x,
                                             r, b_val, from_bal - b_val,
                                             [from_index, to_index]);

        var u = bn128.serialize(utils.u(epoch, keypairs[from_index].x));
        const res = await conman.transfer(C_serialized, D_serialized,
                                          ys, u, proof,
                                          contract_address, opts.sender);

        console.log(res);
        // const events = res.events;
        // for (const [i, v] of Object.entries(events)) {
        //   console.log(i, v);
        // }
      } catch (err) {
        // console.log('error: ', err);
        throw err;
      }
    });

  comm.command('lock <zthacc> <contract_address>')
    .description('lock account to certain address')
    .action(() => {
      console.log('todo: lock account');
    });

  comm.command('unlock <zthacc> <contract_address>')
    .description('unlock account')
    .action(() => {
      console.log('todo: unlock account');
    });
};

exports.after_parse = function after(program) {
};
